using System;
using System.Collections.Generic;
using System.Text;
using System.Globalization;
using System.IO;

namespace Mud.Common.Messages
{
    /// <summary>
    /// Basic unit of transmission that can be sent over the network.
    /// </summary>
    public class BytePackage
    {

        private MemoryStream _stream;


        /// <summary>
        /// Initializes a new instance of the <see cref="T:MessageComposer"/> class.
        /// </summary>
        private BytePackage() 
        {
        }

        /// <summary>
        /// Creates a new package.
        /// </summary>
        /// <returns>New message composer</returns>
        public static BytePackage CreateNew()
        {
            BytePackage composer = new BytePackage();
            composer._stream = new MemoryStream();
            composer._stream.Position = 4; // skip first 4 bytes which will hold the length of the buffer
            return composer;
        }

        /// <summary>
        /// Creates a new package out of the given buffer.
        /// </summary>
        /// <param name="buffer">Buffer to use</param>
        /// <param name="offset">Offset from which to create the buffer</param>
        /// <param name="length">Length of the buffer that should be used for the package.</param>
        /// <param name="composerLength">This parameter will contain the number of bytes that the package will use.</param>
        /// <returns>New package or null if the buffer does not contain the entire message.</returns>
        public static BytePackage Pack(byte[] buffer, int offset, int length, out int composerLength)
        {
            int messageLength = BitConverter.ToInt32(buffer, offset);
            composerLength = 0;
            if (length < messageLength)
                return null;

            BytePackage composer = new BytePackage();
            byte[] copy = new byte[messageLength];
            Buffer.BlockCopy(buffer, offset, copy, 0, messageLength);
            composer._stream = new MemoryStream(copy, 0, messageLength, false);
            composer._stream.Position = 4; // skip the length part
            composerLength = messageLength;
            return composer;
        }

        /// <summary>
        /// Determines whether a given buffer can be packed into a package.
        /// </summary>
        /// <param name="buffer">Buffer</param>
        /// <param name="offset">Offset from which to create the buffer</param>
        /// <param name="length">Length of the buffer that should be used for the package.</param>
        /// <returns>True if the given buffer can be packed into a package.</returns>
        public static bool CanPack(byte[] buffer, int offset, int length)
        {
            int messageLength = BitConverter.ToInt32(buffer, offset);
            if (length < messageLength)
                return false;
            else
                return true;
        }


        /// <summary>
        /// Adds one value to the end of the package.
        /// </summary>
        public BytePackage Enqueue(byte value)
        {
            _stream.WriteByte(value);
            return this;
        }

        /// <summary>
        /// Adds one value to the end of the package.
        /// </summary>
        public BytePackage Enqueue(int value)
        {
            byte[] valueAsBytes = BitConverter.GetBytes(value);
            _stream.Write(valueAsBytes, 0, valueAsBytes.Length);
            return this;
        }

        /// <summary>
        /// Adds one value to the end of the package.
        /// </summary>
        public BytePackage Enqueue(long value)
        {
            byte[] valueAsBytes = BitConverter.GetBytes(value);
            _stream.Write(valueAsBytes, 0, valueAsBytes.Length);
            return this;
        }

        /// <summary>
        /// Adds one value to the end of the message.
        /// </summary>
        public BytePackage Enqueue(double value)
        {
            byte[] valueAsBytes = BitConverter.GetBytes(value);
            _stream.Write(valueAsBytes, 0, valueAsBytes.Length);
            return this;
        }

        /// <summary>
        /// Adds one value to the end of the package.
        /// </summary>
        public BytePackage Enqueue(float value)
        {
            byte[] valueAsBytes = BitConverter.GetBytes(value);
            _stream.Write(valueAsBytes, 0, valueAsBytes.Length);
            return this;
        }

        /// <summary>
        /// Adds one value to the end of the package.
        /// </summary>
        public BytePackage Enqueue(bool value)
        {
            byte[] valueAsBytes = BitConverter.GetBytes(value);
            _stream.Write(valueAsBytes, 0, valueAsBytes.Length);
            return this;
        }

        /// <summary>
        /// Adds one value to the end of the package.
        /// </summary>
        public BytePackage Enqueue(DateTime value)
        {
            return Enqueue(value.ToBinary());
        }
        
        /// <summary>
        /// Adds one value to the end of the package.
        /// </summary>
        public BytePackage Enqueue(string value)
        {
            byte[] valueAsBytes = Encoding.UTF8.GetBytes(value);
            this.Enqueue(valueAsBytes.Length); // write the length first
            _stream.Write(valueAsBytes, 0, valueAsBytes.Length);
            return this;
        }


        /// <summary>
        /// Gets the next value in the package.
        /// </summary>
        public int DequeueByte()
        {
            if (_stream.CanWrite)
                throw new InvalidOperationException("You should not read from BytePackage used for creating a message.");

            return (byte)_stream.ReadByte();
        }

        /// <summary>
        /// Gets the next value in the package.
        /// </summary>
        public int DequeueInt()
        {
            if (_stream.CanWrite)
                throw new InvalidOperationException("You should not read from BytePackage used for creating a message.");

            byte[] valueAsBytes = new byte[sizeof(int)];
            _stream.Read(valueAsBytes, 0, sizeof(int));
            return BitConverter.ToInt32(valueAsBytes, 0);
        }

        /// <summary>
        /// Gets the next value in the package.
        /// </summary>
        public long DequeueLong()
        {
            if (_stream.CanWrite)
                throw new InvalidOperationException("You should not read from BytePackage used for creating a message.");

            byte[] valueAsBytes = new byte[sizeof(long)];
            _stream.Read(valueAsBytes, 0, sizeof(long));
            return BitConverter.ToInt32(valueAsBytes, 0);
        }

        /// <summary>
        /// Gets the next value in the package.
        /// </summary>
        public double DequeueDouble()
        {
            if (_stream.CanWrite)
                throw new InvalidOperationException("You should not read from BytePackage used for creating a message.");

            byte[] valueAsBytes = new byte[sizeof(double)];
            _stream.Read(valueAsBytes, 0, sizeof(double));
            return BitConverter.ToDouble(valueAsBytes, 0);
        }

        /// <summary>
        /// Gets the next value in the package.
        /// </summary>
        public float DequeueFloat()
        {
            if (_stream.CanWrite)
                throw new InvalidOperationException("You should not read from BytePackage used for creating a message.");

            byte[] valueAsBytes = new byte[sizeof(float)];
            _stream.Read(valueAsBytes, 0, sizeof(float));
            return BitConverter.ToSingle(valueAsBytes, 0);
        }


        /// <summary>
        /// Gets the next value in the package.
        /// </summary>
        public bool DequeueBool()
        {
            if (_stream.CanWrite)
                throw new InvalidOperationException("You should not read from BytePackage used for creating a message.");

            byte[] valueAsBytes = new byte[sizeof(bool)];
            _stream.Read(valueAsBytes, 0, sizeof(bool));
            return BitConverter.ToBoolean(valueAsBytes, 0);
        }


        /// <summary>
        /// Gets the next value in the package.
        /// </summary>
        public DateTime DequeueDateTime()
        {
            return DateTime.FromBinary(DequeueLong());
        }

        /// <summary>
        /// Gets the next value in the package.
        /// </summary>
        public string DequeueString()
        {
            if (_stream.CanWrite)
                throw new InvalidOperationException("You should not read from BytePackage used for creating a message.");

            int length = DequeueInt();
            byte[] valueAsBytes = new byte[length];
            _stream.Read(valueAsBytes, 0, length);
            return Encoding.UTF8.GetString(valueAsBytes);
        }

        /// <summary>
        /// Peeks at the next value in the package.
        /// </summary>
        public int PeekInt()
        {
            long position = _stream.Position;
            int value = DequeueInt();
            _stream.Position = position;
            return value;
        }

        /// <summary>
        /// Unpacks this instance into bytes. After unpacking the instance should no longer
        /// be modified.
        /// </summary>
        public byte[] Unpack()
        {
            int position = (int)_stream.Position;
            _stream.Position = 0;
            Enqueue(position);
            _stream.Position = position;
            _stream.Capacity = position;
            return _stream.GetBuffer();
        }
    }
}
